Skip to content

Conversation

@Xinhe-Chen
Copy link
Contributor

@Xinhe-Chen Xinhe-Chen commented Dec 2, 2025

Fixes

This PR adds new features to the price-taker class.

Summary/Motivation:

The unit commitment model in the current price-taker class only supports a single type of startup. However, some generators have different startup types (e.g., hot and cold start) because their physical components heat up and cool down over time, so the time since last shutdown affects how much fuel it costs, and how much wear-and-tear is incurred to bring them back online.

The startup model is based on Knueven, Bernard, James Ostrowski, and Jean-Paul Watson. "On mixed-integer programming formulations for the unit commitment problem." INFORMS Journal on Computing 32, no. 4 (2020): 857-876.

Changes proposed in this PR:

  • Add the method for supporting the various startup types for the idaes price-taker class.
  • Add tests for the method.

Legal Acknowledgement

By contributing to this software project, I agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the license terms described in the LICENSE.txt file at the top level of this directory.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

@codecov
Copy link

codecov bot commented Dec 2, 2025

Codecov Report

❌ Patch coverage is 97.61905% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 73.67%. Comparing base (c8c3df6) to head (cbe6eb9).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
...s/grid_integration/pricetaker/price_taker_model.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1713      +/-   ##
==========================================
- Coverage   77.36%   73.67%   -3.70%     
==========================================
  Files         394      397       +3     
  Lines       64980    65081     +101     
  Branches    10947    10962      +15     
==========================================
- Hits        50274    47949    -2325     
- Misses      12197    14629    +2432     
+ Partials     2509     2503       -6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sufikaur sufikaur added the Priority:Normal Normal Priority Issue or PR label Dec 4, 2025
CONFIG.declare(
"startup_types",
ConfigValue(
domain=dict,
Copy link
Contributor

@radhakrishnatg radhakrishnatg Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest using a more rigorous domain validation method.

def is_valid_startup_types(data):
    if not isinstance(data, dict):
        raise TypeError("Data must be a dict")

    if len(data) == 0:
        raise ConfigurationError("Received an empty dictionary for startup types")

    for key, value in data.items():
        if not isinstance(key, str):
            raise TypeError("key must be a str")

        if not isinstance(value, int):
            raise TypeError("value must be an int")

    # Values must be unique, as they correspond to different startup types
    if len(data.values()) > len(set(data.values())):
        raise ConfigurationError("Startup time for two or more startup types is the same.")

    # Return a dictionary after sorting based on values
    return dict(sorted(data.items(), key=lambda item: item[1]))

You can use this domain validator as:

ConfigValue(domain=is_valid_startup_types, doc="...")

This way, you don't have to check for empty dictionaries in your main code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the codes based on your comments.

CONFIG.declare(
"startup_types",
ConfigValue(
domain=dict,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the above domain validator: is_valid_startup_types


if not startup_transition_time:
# if there is only one startup type, return
# startup_transition_time can be None or empty dict
Copy link
Contributor

@radhakrishnatg radhakrishnatg Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the domain validator, empty dictionaries are no longer valid inputs. You just need to check that it is not None. For improved code readability, I suggest:

if startup_transition_time is None:
    return

return

# multiple startup types
if startup_transition_time:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove this if condition and de-indent the code.

minimum_down_time, startup_transition_time[startup_names[0]]
)

# add a check to ensure the startup time is monotonically increasing.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Domain validator returns a sorted dictionary. So, this check can now be removed.

Copy link
Contributor

@radhakrishnatg radhakrishnatg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a few comments to improve code readability and maintainability. We can discuss the comments in our next meeting.

def is_valid_startup_types(data):
"""Validate if the startup_types received is valid"""

if data is None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is not needed. If the argument value is None, then Pyomo's ConfigDict would not call the domain validator.

Copy link
Contributor

@radhakrishnatg radhakrishnatg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code looks good, but I still have a few minor changes.

)

if self.config.startup_types:
# self.config.startup_types can be None or an empty dict
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor change: For better code readability, please change the condition to if self.config.startup_types is not None:. Also, please remove the comment, as it is no longer applicable (may create confusion for future maintainers).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


# multiple startup types
if startup_transition_time:
# there will be at least two types of startup
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned earlier, the if condition check is now unnecessary. You can remove the check, and remove the additional indentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if not isinstance(data, dict):
raise TypeError("Data must be a dictionary.")

if len(data) == 0:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on your tests, I'm wondering if we should change this to if len(data) <= 1:. This method must be used only when there is more than one startup type. If you have only startup type, then this feels unnecessary (Also, we will have to check the minimum uptime and startup time values to make sure they are consistent). Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think the suggestion makes sense to me. Eq. 54 in Ben's paper once confused me until I asked for clarification from him. If {"hot": 4, "warm": 8, "cold": 12}, when the downtime is between 4 to 8, it is a hot start; 8 to 12, warm start and 12 to infinity, a cold start. This is important to understand eq54, if only one startup_transition_time is given, eq54 will break down.
I will add this example to the comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority:Normal Normal Priority Issue or PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants